﻿// -----------------------------------------------------------------------
// <copyright file="Visitor.cs" company="none">
// Copyright (c) 2012 M. Alberti, xanatos(at)live.it
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
// </copyright>
// -----------------------------------------------------------------------

namespace FluentSerializer
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Runtime.CompilerServices;
    using FluentStatement;
    using Utilities;

    /// <summary>
    /// 
    /// </summary>
    public class Visitor : FluentStatement.Visitor
    {
        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo SerializeT = typeof(Methods).GetMethods().Where(p => p.Name == "Serialize" && p.GetParameters().Length == 2).Single();

        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo SerializeWithType = (from p in typeof(Methods).GetMethods().Where(p => p.Name == "Serialize")
                                                                  let q = p.GetParameters()
                                                                  where q.Length == 3 && q[2].ParameterType == typeof(Type)
                                                                  select p).Single();

        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo SerializeWithDescriptor = (from p in typeof(Methods).GetMethods().Where(p => p.Name == "Serialize")
                                                                        let q = p.GetParameters()
                                                                        where q.Length == 3 && q[2].ParameterType == typeof(Descriptor)
                                                                        select p).Single();

        /// <summary>
        /// 
        /// </summary>
        protected static new readonly Dictionary<MethodInfo, Func<Visitor, MethodCallExpression, Type, MethodInfo, Expression>> Methods = new Dictionary<MethodInfo, Func<Visitor, MethodCallExpression, Type, MethodInfo, Expression>>
        {
            { SerializeT, (p, q, r, s) => p.SerializeTStatement(q, r, s) },
            { SerializeWithType, (p, q, r, s) => p.SerializeTStatement(q, r, s) },
            { SerializeWithDescriptor, (p, q, r, s) => p.SerializeTStatement(q, r, s) },
        };

        /// <summary>
        /// 
        /// </summary>
        protected readonly Serializer Serializer;

        /// <summary>
        /// 
        /// </summary>
        protected readonly Operation Operation;

        /// <summary>
        /// 
        /// </summary>
        protected readonly Type Helper;

        /// <summary>
        /// 
        /// </summary>
        protected readonly ConstantExpression OperationReplaceExpression;

        /// <summary>
        /// 
        /// </summary>
        protected readonly ParameterExpression SizeExpression = Expression.Variable(typeof(int));

        /// <summary>
        /// 
        /// </summary>
        /// <param name="serializer"></param>
        /// <param name="operation"></param>
        /// <param name="helper"></param>
        /// <param name="isUniversal"></param>
        public Visitor(Serializer serializer, Operation operation, Type helper, bool isUniversal)
        {
            this.Serializer = serializer;
            this.Operation = operation;
            this.OperationReplaceExpression = Expression.Constant(operation);
            this.Helper = helper;
            this.IsUniversal = isUniversal;
        }

        /// <summary>
        /// 
        /// </summary>
        public bool IsUniversal { get; protected set; }

        /// <summary>
        /// 
        /// </summary>
        protected ParameterExpression OperationSearchExpression { get; set; }

        /// <summary>
        /// 
        /// </summary>
        protected ParameterExpression Parameters { get; set; }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <param name="returnType"></param>
        /// <returns></returns>
        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "More readable this way")]
        public override Expression StartVisit(Expression expression, Type returnType = null)
        {
            LambdaExpression expression2 = this.ExpressionOfType<LambdaExpression>(expression);

            if (expression2.Parameters.Count != 3)
            {
                throw new NotSupportedException();
            }

            if (expression2.ReturnType != typeof(void))
            {
                throw new NotSupportedException();
            }

            ParameterExpression operation = expression2.Parameters[0];
            ParameterExpression obj = expression2.Parameters[1];
            ParameterExpression parameters = expression2.Parameters[2];

            if (operation.Type != typeof(Operation))
            {
                throw new NotSupportedException();
            }

            this.OperationSearchExpression = operation;
            this.Parameters = parameters;

            Expression expression3 = expression2.Body;

            switch (this.Operation)
            {
                case FluentSerializer.Operation.Deserialize:
                    expression3 = Expression.Block(
                            expression3,
                            Expression.Call(Expression.Constant(null, typeof(Statement)), ReturnT.MakeGenericMethod(returnType), obj));
                    break;

                case FluentSerializer.Operation.Size:
                    expression3 = Expression.Block(
                            new[] { this.SizeExpression },
                            new Expression[] 
                            {
                                Expression.Assign(this.SizeExpression, FluentSerializer.Serializer.Zero),
                                expression3,
                                Expression.Call(Expression.Constant(null, typeof(Statement)), ReturnT.MakeGenericMethod(returnType), this.SizeExpression)
                            });
                    break;
            }

            Expression expression4 = base.StartVisit(expression3, returnType);

            LambdaExpression expression5 = null;

            if (this.IsUniversal)
            {
                switch (this.Operation)
                {
                    case FluentSerializer.Operation.Deserialize:
                        expression5 = Expression.Lambda(
                            expression4,
                            FluentSerializer.Serializer.ReaderExpression,
                            obj);
                        break;

                    case FluentSerializer.Operation.Serialize:
                        expression5 = Expression.Lambda(
                            expression4,
                            FluentSerializer.Serializer.WriterExpression,
                            obj);
                        break;

                    case FluentSerializer.Operation.Size:
                        expression5 = Expression.Lambda(
                            expression4,
                            obj);
                        break;
                }
            }
            else
            {
                switch (this.Operation)
                {
                    case FluentSerializer.Operation.Deserialize:
                        expression5 = Expression.Lambda(
                            expression4,
                            FluentSerializer.Serializer.ReaderExpression,
                            obj,
                            parameters);
                        break;

                    case FluentSerializer.Operation.Serialize:
                        expression5 = Expression.Lambda(
                            expression4,
                            FluentSerializer.Serializer.WriterExpression,
                            obj,
                            parameters);
                        break;

                    case FluentSerializer.Operation.Size:
                        expression5 = Expression.Lambda(
                            expression4,
                            obj,
                            parameters);
                        break;
                }
            }

            return expression5;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected static Expression AssignExpression(Expression obj, Expression expression)
        {
            var obj2 = obj as MemberExpression;

            if (obj2 != null)
            {
                var field = obj2.Member as FieldInfo;

                if (field != null && field.IsInitOnly)
                {
                    if (field.IsStatic)
                    {
                        return Expression.Call(FieldSetStatic(field), expression);
                    }
                    else
                    {
                        return Expression.Call(FieldSet(field), obj2.Expression, expression);
                    }
                }
            }

            return Expression.Assign(obj, expression);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="field"></param>
        /// <returns></returns>
        protected static MethodInfo FieldSet(FieldInfo field)
        {
            // TODO save the field and the resulting setter so that everything can be replaced 
            // in the expression for the Dump()
            Type type = field.DeclaringType;

            bool isVolatile = Array.FindIndex(field.GetRequiredCustomModifiers(), p => p == typeof(IsVolatile)) != -1;

            var pars = new[] { type.IsValueType ? type.MakeByRefType() : type, field.FieldType };

            var setter = new DynamicMethod(string.Empty, typeof(void), pars, type, true);
            ILGenerator il = setter.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);

            if (isVolatile)
            {
                il.Emit(OpCodes.Volatile);
            }

            il.Emit(OpCodes.Stfld, field);
            il.Emit(OpCodes.Ret);
            
            return setter;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="field"></param>
        /// <returns></returns>
        protected static MethodInfo FieldSetStatic(FieldInfo field)
        {
            // TODO save the field and the resulting setter so that everything can be replaced 
            // in the expression for the Dump()
            Type type = field.DeclaringType;

            bool isVolatile = Array.FindIndex(field.GetRequiredCustomModifiers(), p => p == typeof(IsVolatile)) != -1;

            var pars = new[] { field.FieldType };

            var setter = new DynamicMethod(string.Empty, typeof(void), pars, type, true);
            ILGenerator il = setter.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            if (isVolatile)
            {
                il.Emit(OpCodes.Volatile);
            }

            il.Emit(OpCodes.Stsfld, field);
            il.Emit(OpCodes.Ret);

            return setter;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="declaringType"></param>
        /// <param name="method"></param>
        /// <returns></returns>
        protected override bool ProcessMethodCall(Type declaringType, MethodInfo method)
        {
            return declaringType == typeof(Methods) || base.ProcessMethodCall(declaringType, method);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="node"></param>
        /// <param name="declaringType"></param>
        /// <param name="method"></param>
        /// <returns></returns>
        protected override Expression VisitStatementMethod(MethodCallExpression node, Type declaringType, MethodInfo method)
        {
            Func<Visitor, MethodCallExpression, Type, MethodInfo, Expression> method2;

            if (!Methods.TryGetValue(method, out method2))
            {
                return base.VisitStatementMethod(node, declaringType, method);
            }

            Expression expression = method2(this, node, declaringType, method);
            return expression;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="node"></param>
        /// <returns></returns>
        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            Expression expression = base.VisitLambda<T>(node);
            return expression;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (node == this.OperationSearchExpression)
            {
                return this.OperationReplaceExpression;
            }

            return base.VisitParameter(node);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="node"></param>
        /// <param name="declaringType"></param>
        /// <param name="method"></param>
        /// <returns></returns>
        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "More readable this way")]
        protected Expression SerializeTStatement(MethodCallExpression node, Type declaringType, MethodInfo method)
        {
            Expression obj = this.Visit(node.Arguments[1]);

            Type type = obj.Type;
            Type requestedType = null;
            Descriptor descriptor = null;

            if (method == SerializeT)
            {
                requestedType = type;
            }
            else if (node.Arguments[2].Type == typeof(Type))
            {
                requestedType = (Type)Expression.Lambda(node.Arguments[2]).Compile().DynamicInvoke();
            }
            else if (node.Arguments[2].Type == typeof(Descriptor))
            {
                descriptor = (Descriptor)Expression.Lambda(node.Arguments[2]).Compile().DynamicInvoke();
            }
            else
            {
                throw new NotSupportedException();
            }

            if (descriptor == null)
            {
                descriptor = FluentSerializer.Serializer.GetSerializer(this.Serializer, type, requestedType, this.Helper);
                this.IsUniversal &= descriptor.IsUniversal;
            }

            Expression expression = null;

            if (descriptor.IsUniversal)
            {
                switch (this.Operation)
                {
                    case FluentSerializer.Operation.Deserialize:
                        expression = DirectLambdaExpression.Prepare(descriptor.DeserializeExpression, FluentSerializer.Serializer.ReaderExpression, obj);

                        if (descriptor.DeserializationHandling.HasFlag(DeserializationHandling.RequireAssignment))
                        {
                            expression = AssignExpression(obj, expression);
                        }

                        break;

                    case FluentSerializer.Operation.Serialize:
                        expression = DirectLambdaExpression.Prepare(descriptor.SerializeExpression, FluentSerializer.Serializer.WriterExpression, obj);
                        break;

                    case FluentSerializer.Operation.Size:
                        expression = DirectLambdaExpression.Prepare(descriptor.SizeExpression, obj);
                        expression = Expression.AddAssign(this.SizeExpression, expression);
                        break;
                }
            }
            else
            {
                switch (this.Operation)
                {
                    case FluentSerializer.Operation.Deserialize:
                        expression = DirectLambdaExpression.Prepare(descriptor.DeserializeExpression, FluentSerializer.Serializer.ReaderExpression, obj, this.Parameters);

                        // There is a small problem with value types and assignment. With some combinations of expression trees there is an exception.
                        if (descriptor.Type.IsValueType)
                        {
                            ParameterExpression obj2 = Expression.Variable(descriptor.Type);

                            expression = Expression.Block(
                                new[] { obj2 },
                                new Expression[] 
                                { 
                                    Expression.Assign(obj2, expression),
                                    AssignExpression(obj, obj2)
                                });
                        }
                        else
                        {
                            if (descriptor.DeserializationHandling.HasFlag(DeserializationHandling.RequireAssignment))
                            {
                                expression = AssignExpression(obj, expression);
                            }
                        }

                        break;

                    case FluentSerializer.Operation.Serialize:
                        expression = DirectLambdaExpression.Prepare(descriptor.SerializeExpression, FluentSerializer.Serializer.WriterExpression, obj, this.Parameters);
                        break;

                    case FluentSerializer.Operation.Size:
                        expression = DirectLambdaExpression.Prepare(descriptor.SizeExpression, obj, this.Parameters);
                        expression = Expression.AddAssign(this.SizeExpression, expression);
                        break;
                }
            }

            return expression;
        }
    }
}